<?php
//
// SiteVerse CMS (c) 2006,2007,2008 by Konstantin Boyandin <konstantin@boyandin.com>
// Primary library file
//
// $Id: sv.lib.php 2 2009-11-25 03:34:36Z zaurum $

interface iSV_View {
	public function assign($n, $v = null);
	public function assign_by_ref($n, &$v);
	public function display($tn);
	public function fetch($tn);
}

interface iSV_Mailer{
	public function AddCustomHeader($str);
	public function AddAddress($to, $toname);
	public function SetFrom($from, $fromname);
	public function SetSubject($subj);
	public function SetBody($body);
	public function Send();
}

class SV_App {
	public
		$modvar,  	// reference to modules $var public hash
		$defdb,   	// default DB handler
		$defmailer, // default mailer
		$var;     	// variables
	protected
		$view,    // views
		$mailer,  // mailers
		$db,      // database handlers
		$mod,     // modules
		$runrec,     // run records
		$defview; // default View
	
//
// SV_App constructor:
// - sets up important variables
// - initializes, if instructed, DB interface instance
// - initializes, if instructs, default View
//	
	public function __construct($p) {
	// Fields setup
		$this->var = array();
		$this->view = array();
		$this->db = array();
		$this->mod = new stdClass();
		$this->mailer = array();
		$this->defmailer = null;
		$this->defdb = null;
		$this->defview = null;
	// Setting parms
        $this->var['.sv'] = dirname(__FILE__) . DIRECTORY_SEPARATOR;
        $this->var['.lib'] = $this->var['.sv'] . 'lib' . DIRECTORY_SEPARATOR;
        $this->var['.mod'] = array( $this->var['.sv'] . 'module' . DIRECTORY_SEPARATOR );
	// Start caching unless forbidden
		ob_start();
	// Now we scan through the whole array of input parameters and follow this approach: 
	// if for a array key $k we have a method named add_$k, execute it and pass to it
	// the value for the given key. Note: key must contain letters only, no digits and no
	// underscore
		foreach ($p as $k => $v) {
			if (preg_match("/^[a-z]+\$/i", $k)) {
				$m = "add_$k";
				if (method_exists($this, $m)) {
					$this->$m($v);
				}
			}
		}
	} /* SV_App::__construct */

//
// SV_App destructor:
// - calls destructors (by deleting the references) to all the loaded modules
//	
	public function __destruct() {
// Release references
		unset($this->defview);		
		unset($this->defdb);		
// Dispose of modules instances
		$mods = get_object_vars($this->mod);
		foreach ($mods as $k => $v) {
			if (is_object($this->mod->$k)) {
				unset($this->mod->$k);
			}
		}
// Dispose of views		
		foreach ($this->view as $k => $v) {
			if (is_object($v)) {
				unset($this->view[$k]);
			}
		}
// Dispose of db handlers
		foreach ($this->db as $k => $v) {
			if (is_object($v)) {
				unset($this->db[$k]);
			}
		}
	} /* SV_App::__destruct */

//
// SV_App::get_parameter($pname [, $src])
// Loads GET/POST parameter reference and unmagic it if necessary
// If no source array provided, use the current request method
//
    public function get_parameter($v, $src = '') {
        if (!is_string($src)) $src = '';
        if (!in_array($src, array('GET','POST'))) $src = $_SERVER["REQUEST_METHOD"];
        ($src == 'POST') ? $vv =& $_POST[$v] : $vv =& $_GET[$v]; 
        if (ini_get('magic_quotes_gpc')) {
            if (is_array($vv)) {
                return array_map('stripslashes', $vv);
            } else {
                return stripslashes($vv);
            }
        } else {
            return $vv;
        }
    }
    


//
// SV_App::load_class($c,$f)
// Loads class file ($f) and instantiate class $c
// Parameters
//   $c - class name we expect to be available
//   $f - file name; if none provided, look for <sv>/module/$c/$c.module.php
// Returns: reference to the instance of the class
//
	protected function load_class($c, $f = '', $parms = null) {
		$cname = strval($c);
		$fname = strval($f);
		if (!class_exists($cname)) {
			$ffound = true;
			if ($fname == '') {
				$ffound = false;
// Scan all the module paths				
				foreach ($this->var['.mod'] as $pp) {
					$fname = $pp . "$cname/$cname.module.php";
					if (file_exists($fname)) {
						$ffound = true;
						break;
					}
				}	
			}
			if ($ffound) {
				include($fname);
			} else {
				throw new Exception("No class file $fname for $cname");
				return null;
			}
		}
		if (class_exists($cname)) {
// If additional parameters provided, submit them to the constructor			
			if (is_array($parms)) {
				$a =& new $cname($parms);
			} else {
				$a =& new $cname();
			}
			return $a;
		} else {
			throw new Exception("No class $cname found");
			return null;
		}
	}

//
// SV_App::http_redirect($addr)
// Constructs correct URL and sends header to redirect to it
//
    public function http_redirect($address = "/") {
        $https = false;
        $stdport = true;
        $srvport = $_SERVER['SERVER_PORT'];
        $rc = "";
        if (!preg_match("#^[a-z]+://#", $address)) {
// http? https?
            if (isset($_SERVER['SSL_PROTOCOL'])) {
                $https = true;
                if ($srvport != '443') {
                    $stdport = false;
                }
            } else {
                if ($srvport != '80') {
                    $stdport = false;
                }
            }
// Construct the URL
            if ($https) {
                $rc = "https://";
            } else {
                $rc = "http://";
            }
            $rc .= $_SERVER['SERVER_NAME'];
            if (!$stdport) {
                $rc .= ":$srvport";
            }
            if (substr($address, 0, 1) != "/") {
                $rc .= "/";
            }
            $address = $rc . $address;
        }
        header("Location: $address");
        exit(0);
    } /* SV_App::http_redirect */

//
// SV_App: add_mailer($vars)
// Adds new mailer to the $mailer list and make it default one
// Parameters:
//  	$vv - (mailerName:str => constructor arguments:array)
// Note: one would be searching for lib/mailerName.mailer.php  file
// and for SV_Mailer_mailerName class (that implements iSV_Mailer
// interface).
 //
 	protected function add_mailer($vv){
// Parameter must be an array, we only use the first key of it to
// instantiate the corresponding  View
		if (is_array($vv)) {
			foreach ($vv as $vkey => $vval) {
				$k = $vkey; $v = $vval; break;
			}
		} else {
			throw new Exception("No valid parameters for a View");
		}
// Name must be valid, it must start with a letter
		if (!preg_match("/^[a-z][_a-z0-9]*\$/i", $k)) {
			throw new Exception("Invalid Mailer name infix $k");
		}
// Checks passed, build View name and instantiate it		
	   	$vfn = $this->var['.lib'] . $k . ".mailer.php";
	   	$cfn = "SV_Mailer_$k";
	   	if (file_exists($vfn)) {
			include_once($vfn);
			if (class_exists($cfn)) {
				$this->mailer[] = new $cfn($v);
				unset($this->defmailer);
				$this->defmailer = &$this->mailer[count($this->mailer)-1];
			} else {
				throw new Exception("No Class $cfn");
			}
		} else {
			throw new Exception("No Mailer $k" );
		}
	}

//
// SV_App: add_var($vars);
// Loads provided vars into $this->var hash
// Parameters:
//   $vars array (hash) with the desired vars
//	
	protected function add_vars($vars) {
		if (is_array($vars)) {
			foreach ($vars as $k => $v) {
				if (preg_match("/^[a-z][_a-z0-9]+\$/i", $k)) {
					$this->var[$k] = $v;
				}
			}
		} else {
			throw new Exception("add_var parameter must be an array");
		}
	}
	
//
// SV_App: add_view($vn);
// Adds a new View to the Views list and makes it default one
// Parameters:
//   $vn view class name part (looked as lib/$tn.view.php)
//	
	protected function add_view($vv) {
// Parameter must be an array, we only use the first key of it to
// instantiate the corresponding  View
		if (is_array($vv)) {
			foreach ($vv as $vkey => $vval) {
				$k = $vkey; $v = $vval; break;
			}
		} else {
			throw new Exception("No valid parameters for a View");
		}
// Name must be valid, it must start with a letter
		if (!preg_match("/^[a-z][_a-z0-9]*\$/i", $k)) {
			throw new Exception("Invalid View name infix $k");
		}
// Checks passed, build View name and instantiate it		
	   	$vfn = $this->var['.lib'] . $k . ".view.php";
	   	$cfn = "SV_View_$k";
	   	if (file_exists($vfn)) {
			include_once($vfn);
			if (class_exists($cfn)) {
				$this->view[] = new $cfn($v);
				unset($this->defview);
				$this->defview = &$this->view[count($this->view)-1];
			} else {
				throw new Exception("No Class $cfn");
			}
		} else {
			throw new Exception("No View $k" );
		}
	} /* SV_App::add_view */

//
// SV_App: add_db($dsn);
// Adds a new DB to the DBs list and makes it default one
// Parameters:
//   $dsn DSN (see Pear docs) of the DB connection
//	
	protected function add_db($dsn) {
        if (preg_match("#^([a-z0-9]+)://((.*?)(:(.*?))?@)?(([^/:]+)(:([0-9]+))?)/([_a-z0-9]+)(\\+([_a-z0-9]+))?\$#i", $dsn, $pcre)) {
            $db = array(
                'engine' => strtolower($pcre[1]),
                'user' => $pcre[3],
                'password' => $pcre[5],
                'host' => $pcre[7],
                'port' => $pcre[9],
                'database' => $pcre[10],
				'prefix' => ''
            );
			if (isset($pcre[12])) $db['prefix'] = $pcre[12];
            $modname = 'DB' . $db['engine'];
            $fname = $this->var['.lib'] . 'db.' . $db['engine'] . '.php';
			$this->db[] = $this->load_class($modname, $fname, $db);
			unset($this->defdb);
			$this->defdb = &$this->db[count($this->db)-1];
        } else {
            throw new Exception("Invalid DSN $dsn");
        }
	} /* SV_App::add_db */

//
// SV_App: add_include($ipath);
// Sets include_path based on 'include' value passed to the constructor
// Parameters:
//   $ipath include path to set
//	
	protected function add_include($ipath) {
		$i = strval($ipath);
		ini_set("include_path", $i);
	} /* SV_App::add_include */
	
//
// SV_App: add_run($rrecs);
// Sets 'run records' controlling the application behaviour
// Parameter:
//   $rrecs - array of run records
// Every run record is an array containing this key/value pairs:
// Key: path pattern - if the supplied path matches the pattern, run this record
//    Note: if run record with path pattern '*' exists, it is run always
//    Note: if run record with path pattern '-' exists, it is used for non-existing patterns
// Value: array of the following
// [0] list of modules to load (array of strings)
// [1] access expression in the form of 'modname::methodname'; if returns true, it's OK to run
// [2] template name to use (beneath templates directory)
//	
	protected function add_run($rrecs) {
		if (is_array($rrecs)) {
			$this->runrec = $rrecs;
		}
	} /* SV_App::add_include */
	
//
// SV_App: add_session($s);
// Enables, if necessary, sessions
// Parameters (keys of the array passed):
//   'enable': if true, enable session. Default is false.
//   'name': session name, to distinguish from other posssible sessions. Defauklt is that from php.ini
//   'ttl': TTL, if set below 300 seconds, is set to 300. Default is that from php.ini
//   'handler': session handler. Default is 'files'. If different from default one, method tries to
//              load the source as <libdir>/<handler>.session.php
//	
	protected function add_session($s) {
		if (is_array($s)) {
			$this->var['sess_enabled'] = $sess_enabled = false;
			if (isset($s['enabled'])) {
				$sess_enabled = $s['enabled'];
			}
			if ($sess_enabled) {
// Load handler, if exists and not 'files', from library directory
				if (isset($s['handler'])) {
					$sess_handler = strval($s['handler']);
					if (preg_match("/^[a-z][_a-z0-9]*\$/i", $sess_handler)) {
						if ($sess_handler != 'files') {
							$fname = $this->var['.lib'] . $sess_handler . ".session.php";
							if (file_exists($fname)) {
								require_once($fname);
							} else {
								throw new Exception("No session handler file $fname");
								return false;
							}
						}
// If we are here, we may enable session. Setting id...
						$session_name = session_name();
						if (isset($s['name'])) {
							$session_name = strval($s['name']);
							if (preg_match("/^[_a-z0-9]+\$/i", $session_name)) {
								session_name($session_name);
							} else {
		            			throw new Exception("Invalid session id $session_id");
							}
						}
// Set timeout
						$session_ttl = ini_get("session.gc_maxlifetime");
						if (isset($s['ttl'])) {
							$session_ttl = intval($s['ttl']);
							if ($session_ttl < 300) $session_ttl = 300;
						}
						$this->var['sess_ttl'] = $session_ttl;
						ini_set("session.gc_maxlifetime", $session_ttl);
// Finally, enable session
						session_start();
// Now let's create session-version modvars
						if (!isset($_SESSION['sv'])) {
							$_SESSION['sv'] = array('cls' => array());
						} else {
							if (!isset($_SESSION['sv']['cls'])) {
								$_SESSION['sv']['cls'] = array();
							}
						}
						$this->modvar =& $_SESSION['sv']['cls'];
					} else {
            			throw new Exception("Invalid session handler name $sess_handler");
						return false;
					}
				}
			}
		}
		return false;
	} /* SV_App::add_session */
	
//
// SV_App: add_modpath($mpath);
// Sets adds array of paths used to lookup for modules
// Parameters:
//   $mpath paths separated by PATH_SEPARATOR
//	
	protected function add_modpath($mpath) {
		$m = strval($mpath);
		$ma = explode(PATH_SEPARATOR, $mpath);
		if (is_array($ma)) {
			$mold = $this->var['.mod'][0];
			$this->var['.mod'] = array();
			foreach ($ma as $k => $v) {
				if (file_exists($v)) {
// Make sure it ends with DIRECTORY_SEPARATOR
					if (substr($v, -1) == DIRECTORY_SEPARATOR) {
						$this->var['.mod'][] = $v;
					} else {
						$this->var['.mod'][] = $v . DIRECTORY_SEPARATOR;
					}
				}
			}
// ...add the original path
			$this->var['.mod'][] = $mold;
		}
	} /* SV_App::add_modpath */

//
// SV_App::load_module($mname)
// Loads the module, if not yet loaded, and updates the internal objects for accessing it quickly
// Returns the reference to the module (singleton) instance
//
	public function load_module($mname) {
// Sanity check
		if (preg_match("/^[a-z][_0-9a-z]*\$/i", $mname)) {
			if (isset($this->mod->$mname)) {
				$m =& $this->mod->$mname;
// If loaded, must be a subclass of SV_Mod				
				if (is_subclass_of($m, 'SV_Mod')) {
					return $m;
				} else {
		            throw new Exception("Class $mname instance is not derived properly");
				}
			} else {
// Not yet loaded, load it
				$m = $this->load_class($mname, '', array(&$this));
				$clsname = $mname;
				if (is_null($m)) {
// No such module
		            throw new Exception("No module $mname");
					return null;
				} else {
// Module loaded successfully
					$this->mod->$mname =& $m;
					if (!isset($this->modvar[$mname])) {
						$this->modvar[$mname] = array();
					}
					$m->var =& $this->modvar[$mname];
					$m->cls = $clsname;
					return $this->mod->$mname;
				}
			}
		} else {
            throw new Exception("Invalid module name $mname");
			return null;
		}
	} /* SV_App::load_module */
	
//
// SV_App run();
// Runs the application: looks up the relevant action line and executes it
//
    public function run() {
// Add default basic view if none specified
		if (count($this->view) <= 0) {
			$this->add_view(array('basic' => null));
		}		
// Replace with send_headers()		
		header("Content-Type: text/html; charset=utf-8");
        $date = gmdate('D, d M Y H:i:s', time()) .' GMT';
        $etag = '"'. md5($date) .'"';
        if (false) { // guest
            header("Last-Modified: $date");
            header("ETag: $etag");
            header("Expires: Sun, 19 Nov 1978 05:00:00 GMT");
            header("Cache-Control: must-revalidate");
        } else { // authenticated
            header("Expires: Sun, 19 Nov 1978 05:00:00 GMT");
            header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
            header("Cache-Control: no-store, no-cache, must-revalidate");
            header("Cache-Control: post-check=0, pre-check=0", false);
            header("Pragma: no-cache");
        }
// Now perform run checks
// Load mandatory module(s)
		if (isset($this->runrec['*'])) {
			foreach ($this->runrec['*'][0] as $v) {
				$this->load_module($v);
			}
		}
// If method is GET, translate query string, if not - REQUEST_URI
		if ($_SERVER["REQUEST_METHOD"] == 'GET') {
			$q = $this->get_parameter('q');
		} else {
			$q = substr($_SERVER['REQUEST_URI'], 1);
		}
		$qfound = false;		
		if (is_null($q)) $q = '/'; $nq = array('*','-');
		$this->defview->assign_by_ref('appvar', $this->var);
		foreach ($this->runrec as $k => $v)	{			
			if (!in_array($k,$nq)) { // non-special values
				if (preg_match("#^$k\$#i", $q, $qarr)) {
					$qfound = true;
// Execute check statement
					if (preg_match("/^([a-z][_a-z0-9]*)::([a-z][_a-z0-9]*)\$/i", $v[1], $carr)) {
						$cmod = $carr[1]; $cmet = $carr[2]; $cok = false;
						$cm = $this->load_module($cmod);
						if (isset($cm)) {
							if (method_exists($cm, $cmet)) {
								$cok = $cm->$cmet();
							}
						}
						if (!$cok) {
							$qfound = false; $q = '-';
							break;
						}
					}
// Set current template
					$this->var['template'] = $v[2];
// Matching pattern - execute the module(s) run method
					$this->var['.args'] =& $qarr;
					foreach ($v[0] as $mn) {
						$mmod = $this->load_module($mn);
						$mmod->run($qarr);
					}
// No more lookups					
					break;
				}
			}
		}
		
// If no match, load the default missing ('-')
		if (!$qfound) {
			if (isset($this->runrec['-'])) {
				$this->var['template'] = $this->runrec['-'][2];
			}
		}
// Now that everything's processed, display the set template
		$this->defview->assign_by_ref('var', $this->modvar);
		$this->defview->assign_by_ref('server', $_SERVER);
		$this->defview->display($this->var['template']);
	} /* run */
	
    public function make_random_string($slen, $ssrc = "abcdef0123456789") {
        $rc = ''; $srclen = strlen($ssrc) - 1;
        for ($cnt = 1; $cnt <= $slen; $cnt++) {
            $rc .= substr($ssrc, rand(0, $srclen), 1);
        }
        return $rc;
    } /* make_random_string */

	public function is_valid_email($email) {
		$lemail = strval($email);
		if (preg_match("#^([0-9]+|[-_a-z0-9.]+@([0-9a-z][-0-9a-z]*?[0-9a-z]\\.)+([a-z]{2,6}))\$#i", $lemail)) { 
			return true;
		} else {
			return false;
		}
	} /* is_valid_email */
//	
// SV_App send_email($addr, $tmpl)
// Send mail using defmailer
//
	public function send_email($addr, $tmpl) {                
        if (empty($this->defmailer)){
// Default mailer wasn't defined: 
			throw new Exception("Mailer wasn't set in SV_App constructor, exit");
		}
		$message = $this->defview->fetch($tmpl);
		//echo $message; // debug
		$mailer = &$this->defmailer;
// echo "<pre>"; echo htmlspecialchars($message); "</pre>"; exit(0);
// Parse the email        
        $flines = split("\r?\n", $message);
        $eoh = false; $body = ''; $headers = '';
        foreach ($flines as $v) {
            $v = rtrim($v);
            if ($v == '') {
                if ($eoh) {
                    $body .= "\n"; 
                } else {
                    $eoh = true;
                }
            } else {
                if ($eoh) {
                    $body .= (($body != '') ? "\n" : "") . $v;
                } else {
// Headers are processed here. Look for 'To', 'From' and 'Subject' lines
                    if (preg_match('/^subject:(.*)$/i', $v, $rerc)) {
                        $subj = trim($rerc[1]);
                    } elseif (preg_match('/^(from|to):[ ]*("([^"]+)"[ ]*)?<?([^>]*)>?$/i', $v, $rerc)) {
                        $f = strtolower($rerc[1]); $faddr = $f; $fname = $f . 'name';
                        $$fname = trim($rerc[3]);
                        $$faddr = trim($rerc[4]);
                        unset($f, $faddr, $fname);
                    } else {
                        $mailer->AddCustomHeader($v);
                        $headers .= (($headers != '') ? "\r\n" : "") . $v;
                    }
                }
            }
        }
// Body will always end with new line
        $body .= "\n";
// Send the resulting email
        $mailer->AddAddress($to, $toname);
        $mailer->SetFrom($from, $fromname);
        $mailer->SetSubject($subj);
        $mailer->SetBody($body);
        $rc = $mailer->Send();
        return $rc;
        
//        echo "$rc " . $mailer->ErrorInfo; exit(0); 
//        @mail($to, $subj, $body, $headers);
    }	
	
	public function assign_var($n, $v) {
		if (is_object($this->defview)) {
			$this->defview->assign($n, $v);
		}
	} // assign_var
} /* SV_App */

class SV_Mod {
	public
		$app = null,
		$cls,
		$var;
	protected
		$act = array();
		
	public function __construct($arr) {
		$this->app = $arr[0];
	}
	
	public function __destruct() {
		if (isset($this->var['msgttl'])) {
			if ((--$this->var['msgttl']) <= 0) {
				unset($this->var['msgval']);
			}
		}
	}

	public function set_message($msgval, $msgcnt = 1) {
		$this->var['msgttl'] = $msgcnt;
		$this->var['msgval'] = $msgval;
	} /* SV_Mod::set_message */
	
	public function run(&$qarr) {
// Map action key to method name
		if (count($qarr) > 1) {
			$action_key = strval($qarr[1]); // if regular expressions are used, the very first group must match action name
		} else {
			$action_key = strval($qarr[0]); // ...or it IS action name
		}
// Get action prefix
		$apref = 'action_'; $aname = '';
		if (isset($this->app->var['actionpref'])) {
			if (preg_match("/^[a-z][_a-z0-9]*\$/i", $this->app->var['actionpref'])) {
				$apref = $this->app->var['actionpref'];
			} else {
	            throw new Exception("Invalid action prefix " . $this->app->var['actionpref']);
			}
		}
// Look for the action_$clsname	among passed parameters and if set, use as action key
		$action_parm = $this->app->get_parameter($apref . $this->cls);
		if (!isset($action_parm)) $action_parm = '';
		if (strval($action_parm) != '') {
			$action_key = $action_parm;
		}
// Map, if possible action key to action name
		if (isset($this->act[$action_key]))	{
			$aname = $apref . $this->act[$action_key];
			if (method_exists($this, $aname)) {
				$this->$aname($qarr);
			}
		}	
		return $aname;
	}
}
